Document the two size-grid quadrature schemes - #477
Merged
Conversation
…el() Under `second_order_w(params) <- TRUE`, `setPredKernel()` builds `ft_pred_kernel_e` as the predation kernel integrated over the prey bin, dividing by `beta - 1` so that the plain `w * dw` supplied by the prey vector cancels. `mizerEncounter()` accordingly keeps the point weight. `getDiet()` did not: it weighted its prey vectors with the bin-averaged `w`, applying the prey-bin quadrature a second time. The result was uniformly too large by `(1 + beta) / 2` — 9.7% for `NS_params`. Both branches (FFT and custom kernel) were affected. The default `proportion = TRUE` hid it, because a uniform factor divides out in the normalisation. `getTrophicLevel()` had the same mismatch plus a second one: its trophic-level-weighted numerator was built from the point-sampled `getPredKernel()` while its denominator came from `getEncounter()`, which uses the bin-integrated kernel once the flag is on. The two are then no longer the same integral and the trophic levels moved by up to 0.06. Both now use the plain point weight `w * dw`. New internal `encounter_kernel()` returns the kernel `mizerEncounter()` actually uses — recovered from `ft_pred_kernel_e` by inverse FFT, so it cannot drift from whatever quadrature `setPredKernel()` chose — and `getTrophicLevel()` builds its numerator from that. `getPredKernel()` is refactored onto the shared `expand_kernel_offsets()` helper; its behaviour is unchanged. Tests cover the `encounter_kernel()`/`getEncounter()` identity, the diet-sums-to-consumption identity, and a predator whose prey all have trophic level 1 coming out at exactly 2 — each across both settings of the flag and both kernel branches. All fail against the previous code. Fixes #474 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `second_order_w` flag was documented in the FFT vignette from the kernel's side, but nothing said where each bin integral is performed across the model as a whole. That gap is what let #474 through: the prey-bin quadrature is folded into the predation kernel at setup, so a summary function that bin-averages its prey weight as well applies it twice, and the resulting uniform (1 + beta) / 2 factor cancels in any proportion. numerical_details.qmd gains: - "Applying each bin integral exactly once", inside the existing "Point values and bin averages" section. Derives the encounter kernel weight, explains why the `w_p dw_p` in the run-time sum is a normalisation rather than a first-order quadrature weight, and tabulates which factor of each integral is bin-integrated and in which setter. - "The `second_order_w` switch", a new section: the two independent entries, a full FALSE-vs-TRUE table (including the rows that stay point values), and a recipe for making your own diagnostic second-order accurate, ending with the identity check and how to read off (1 + beta) / 2 versus 2 / (1 + beta) when it fails. It also drops a paragraph that had been duplicated verbatim. .claude/skills/size-grid-integrals.md turns this into a decision procedure for anything that integrates over the size grid: four cases, the traps (including both #474 failures), the verification steps, and the helper table. AGENTS.md gets the architectural note that the model has two discretisations and a pointer to the skill. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gustavdelius
force-pushed
the
fix/getdiet-second-order-quadrature
branch
from
August 12, 2026 10:18
bfbb572 to
ada0c25
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #476. Based on that branch, so this PR's diff shows only the documentation; GitHub will retarget it to
masteronce #476 merges.Why
second_order_wwas documented invignette("fft")from the kernel's side, but nothing said where each bin integral is performed across the model as a whole. That gap is what let #474 through: the prey-bin quadrature is folded into the predation kernel at setup, so a summary function that bin-averages its prey weight as well applies it twice — and because the resulting(1 + beta) / 2factor is uniform, it cancels in any proportion and nothing complains.vignettes/numerical_details.qmd"Applying each bin integral exactly once", a new subsection inside the existing Point values and bin averages. Derives the encounter kernel weight, explains why the
w_p dw_pin the run-time sum is a normalisation the kernel has already divided out rather than a first-order quadrature weight left on the table, and tabulates which factor of each of mizer's integrals is bin-integrated and in which setter. Ends with the two corollaries — never averageNordw, and a decomposition borrows its parent rate's quadrature — and the note thatgetPredKernel()is not the kernel the convolution uses once the flag is on."The
second_order_wswitch", a new section. The two independent entries and why they are independent; a fullFALSEvsTRUEtable including the rows that stay point values (the growth-type rates, andgetN()); and a recipe for making your own diagnostic second-order accurate, split into the plain-integral case and the built-from-rates case. It closes with the identity check and how to read off(1 + beta) / 2versus2 / (1 + beta)when it fails.Also drops a paragraph that had been duplicated verbatim in the existing section.
All code snippets were run and produce the stated output; the vignette renders with all cross-references resolving.
.claude/skills/size-grid-integrals.mdThe same material as a decision procedure, in the style of the existing skills: the rule, four cases (plain integral / derived from rates / inside the convolution / new rate setter), the traps, the verification steps, and a helper table. Two things in it go beyond what #474 needed:
g,e, encounter and feeding level are point values under both schemes.It defers the theory to the vignette rather than duplicating it, the way
species-param-defaults.mddefers its table todefault_parameters.qmd.AGENTS.mdAn architectural note that the model has two discretisations and that the default hides the second, plus a pointer to the skill. This is the part that would have prevented the bug: reading
getDiet()gave no hint the flag existed.🤖 Generated with Claude Code